﻿using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using System.Security.Cryptography;
using ClientNodeLib.ServerNodeLib;

namespace ClientNodeLib
{
	/// <summary>
	/// Manages an actual "job" running on the node.
	/// Does everything from download resources, execute commands, and post results.
	/// 
	/// This class exposes delegates for transitions across different stages.  
	/// Use the delegates for hooking into this; nothing else is stable
	/// 
	/// </summary>
	public class BatchJob
	{
		#region data members
		/// <summary>
		/// The server-provided information about the batch
		/// </summary>
		private ServerNodeLib.BatchInfo info;
		/// <summary>
		/// The async thread that the batch runs in.
		/// </summary>
		private System.Threading.Thread batchThread;
		/// <summary>
		/// The build-as-we-go result object of the current job
		/// </summary>
		private ResultInfo result;
		#endregion

		public BatchJob(BatchInfo info)
		{
			if (info == null)
				throw new ArgumentNullException("info", ExceptionMessages.ArgumentNull);
			this.info = info;
		}
		/// <summary>
		/// The server-assigned guid of the batch
		/// </summary>
		public Guid BatchId
		{
			get
			{
				return this.info.BatchId;
			}
		}
		/// <summary>
		/// Returns the server-provided information on the batch job
		/// </summary>
		public BatchInfo BatchInfo
		{
			get
			{
				return this.info;
			}
		}
		/// <summary>
		/// True if the job is running.
		/// Running means downloading resources, executing comands, and queueing up results.
		/// </summary>
		/// <returns>True if the job is running</returns>
		public bool IsRunning()
		{
			return (batchThread != null && batchThread.IsAlive);
		}
		/// <summary>
		/// Returns true if the two batch ids are the same
		/// False if different types.
		/// </summary>
		/// <param name="obj">Preferably a batch object</param>
		/// <returns></returns>
		public override bool Equals(object obj)
		{
			BatchJob other = obj as BatchJob;
			if (other == null)
				return false;
			else
				return other.info.BatchId.Equals(this.info.BatchId);
		}
		/// <summary>
		/// Returns the hash code of the batch id
		/// Overridden per good coding practices
		/// </summary>
		/// <returns></returns>
		public override int GetHashCode()
		{
			return this.info.BatchId.GetHashCode();
		}
		/// <summary>
		/// Returns the string representation per the setting JobNameFormat
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			return String.Format(ClientNodeSettings.Default.JobNameFormat, this.info.BatchId.ToString());
		}

		/// <summary>
		/// Starts the job async.  Job runs in a separate thread.
		/// </summary>
		public void Start()
		{
			if (this.IsRunning())
				throw new InvalidOperationException(ExceptionMessages.BatchAlreadyRunning);

			batchThread = new System.Threading.Thread(this.AsyncDoBatch);
			batchThread.Start();
		}
		/// <summary>
		/// Stops the job thread.
		/// You can restart it if you want; restarting not tested!
		/// </summary>
		public void Stop()
		{
			if (batchThread != null)
				batchThread.Abort();
		}

		/// <summary>
		/// Head method that
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		protected void AsyncDoBatch()
		{
			CommonLib.Tracer.StartActivity("AsyncDoBatch " + this.info.BatchId);
			result = new ResultInfo();
			try
			{
				result.StartTime = DateTime.Now;
				result.BatchId = this.info.BatchId;
				result.CommandResults = new List<CommandResult>();
				result.ResourceResults = new List<ResourceResult>();

				if (OnBatchJobStart != null)
					OnBatchJobStart.Invoke(info.BatchId);

				if (LoadResources())
				{
					CommonLib.Tracer.ReportInformation("Successfully loaded resources");
					ExecuteCommands();
					result.IsSuccess = true;
					result.SummaryMessage = ExceptionMessages.BatchCompleted;
				}
				else
				{
					CommonLib.Tracer.ReportWarning(ExceptionMessages.FailedDownloadResources);
					result.IsSuccess = false;
					result.SummaryMessage = ExceptionMessages.FailedDownloadResources;
				}
				
				
			}
			catch (System.Threading.ThreadAbortException)
			{
				result.IsSuccess = false;
				CommonLib.Tracer.ReportError("Thread abort raised");
				result.SummaryMessage = ExceptionMessages.BatchAbortMessage;
			}
			catch (Exception ex)
			{
				result.IsSuccess = false;
				CommonLib.Tracer.ReportError("General error", ex);
				result.SummaryMessage = ex.Message;
			}
			finally
			{
				result.EndTime = DateTime.Now;

				// let the scheduler know that the job completed
				if (OnBatchJobComplete != null)
					OnBatchJobComplete.Invoke(result);
				CommonLib.Tracer.EndActivity("AsyncDoBatch " + this.info.BatchId);
			}
		}
		/// <summary>
		/// Checks each resource the server asked we have in place.
		/// If the resource does not exist OR if the hash is diff,
		/// the code downloads the server copy.
		/// </summary>
		/// <returns>True if all resources successfully transfered</returns>
		protected bool LoadResources()
		{
			CommonLib.Tracer.StartActivity("Load Resources");
			MD5 md5 = new MD5CryptoServiceProvider();

			// WCF client to connect to server
			SchedulerServerClient scheduleServer = SimpleSchedulerClient.BuildWcfClient();
			if (scheduleServer == null)
				return false;

			foreach (ResourceInfo resource in this.info.BatchResources)
			{
				bool doDownload = true;
				
				ResourceResult resResult = new ResourceResult();
				resResult.Path = resource.DestName;
				if (System.IO.File.Exists(resource.DestName))
				{
					using (System.IO.FileStream fs = new System.IO.FileStream(resource.DestName, System.IO.FileMode.Open))
					{
						byte[] hash = md5.ComputeHash(fs);
						if (Convert.ToBase64String(hash).Equals(resource.Md5Hash))
						{
							resResult.AlreadyExists = true;
							doDownload = false;
						}
					}
				}

				if (doDownload)
				{
					CommonLib.Tracer.ReportInformation("Need to download file " + resource.DestName);
					long totalDownloaded = 0;
					using(System.IO.FileStream fs = new System.IO.FileStream(resource.DestName, System.IO.FileMode.Create))
					{
						int chunkNum = 0;
						while(totalDownloaded < resource.DestSize)
						{
							byte[] data = scheduleServer.GetResourceChunk(this.info.BatchId, resource.DestName, chunkNum++);
							totalDownloaded += data.Length;
							fs.Write(data, 0, data.Length);
						}
						fs.Close();
					}

					// validate it was transfered properly:
					using (System.IO.FileStream fs = new System.IO.FileStream(resource.DestName, System.IO.FileMode.Open))
					{
						byte[] hash = md5.ComputeHash(fs);
						if (Convert.ToBase64String(hash).Equals(resource.Md5Hash))
						{
							resResult.TransferSuccessfully = true;
						}
						else
						{
							resResult.TransferSuccessfully = false;
							throw new ArgumentException("File has did not match server oracle");
						}
					}

					
					if (OnBatchJobDownloadResource != null)
						OnBatchJobDownloadResource.Invoke(this.info.BatchId, resource.DestName);
				}
				
				this.result.ResourceResults.Add(resResult);
			}
			scheduleServer.Close();
			CommonLib.Tracer.StartActivity("End Resources");
			return true;
		}
		/// <summary>
		/// Executes each command as if at the command line
		/// 
		/// A command is successful if
		/// 1) It does not throw an exception
		/// 2) It completes in the time window IFF server said that was important
		/// 3) Not aborted by external code
		/// </summary>
		/// <returns>True if all commands execute successfully</returns>
		protected bool ExecuteCommands()
		{
			CommonLib.Tracer.StartActivity("Execute Commands");
			result.StartTime = DateTime.Now;
			System.Diagnostics.Process p = null;

			foreach (CommandInfo c in this.info.Commands)
			{
				CommandResult comRes = new CommandResult();
				comRes.Command = c.Command;
				comRes.Arguments = c.Arguments;
				comRes.StartTime = DateTime.Now;
				CommonLib.Tracer.ReportInformation(String.Format("Starting command '{0} {1}' at {2}", comRes.Command, comRes.Arguments, comRes.StartTime.ToString()));
				Int32 MaxTimeDecaSeconds = c.MaxTimeSeconds * 10;
				Int32 MinTimeDecaSeconds = c.MinTimeSeconds * 10;
				try
				{
					if (OnBatchJobCommandStep != null)
						OnBatchJobCommandStep.Invoke(info.BatchId, c.Command, c.Arguments);

					p = new System.Diagnostics.Process();
					p.StartInfo = new System.Diagnostics.ProcessStartInfo(c.Command, c.Arguments);
					p.StartInfo.WorkingDirectory = c.Path;

					p.Start();

					Int32 timeoutCount = MaxTimeDecaSeconds;
					while (timeoutCount >= 0)
					{
						p.Refresh();
						if (p.HasExited)
							break;
						timeoutCount--;
						System.Threading.Thread.Sleep(100);
					}

					Int32 ExecutionTimeDecaSeconds = MaxTimeDecaSeconds - timeoutCount;

					if (p.HasExited == false)
					{
						// first attempt to gracefully close the application
						timeoutCount = MaxTimeDecaSeconds;
						while (p.HasExited == false && timeoutCount >= 0)
						{
							p.Refresh();
							p.CloseMainWindow();
							timeoutCount--;
							System.Threading.Thread.Sleep(100);
						}
						// second kill it outright
						if (p.HasExited == false)
						{
							p.Kill();
						}
					}

					if (ExecutionTimeDecaSeconds >= MaxTimeDecaSeconds || ExecutionTimeDecaSeconds < MinTimeDecaSeconds)
						throw new TimeoutException(String.Format(ExceptionMessages.ProcessTimeoutMessage, c.Command, c.Arguments, ExecutionTimeDecaSeconds / 10));

					comRes.IsSuccess = true;
				}
				catch (TimeoutException)
				{
					comRes.IsSuccess = false;
					CommonLib.Tracer.ReportInformation("Timeout exception occurred");
					if (c.ErrorOnTimeOut)
					{
						CommonLib.Tracer.ReportError("Timeout exception will be propagated up since the command should error on timeout.");
						throw;
					}
				}
				catch (System.Threading.ThreadAbortException)
				{
					CommonLib.Tracer.ReportWarning("Thread abort received");
					comRes.IsSuccess = false;
					throw;
				}
				catch (Exception ex)
				{
					if (p != null)
					{
						if(p.HasExited == false)
							p.Kill();
					}
					comRes.IsSuccess = false;
					CommonLib.Tracer.ReportWarning("General exception", ex);
					if (c.BreakOnException)
					{
						CommonLib.Tracer.ReportError("Exception will be propagated up since the command should error on exception.");
						throw;
					}
				}
				finally
				{
					if (p != null && p.HasExited == false)
						p.Kill();
					comRes.EndTime = DateTime.Now;
					CommonLib.Tracer.ReportInformation("Command ended at " + comRes.EndTime);
					CommonLib.Tracer.ReportInformation("Command took " + comRes.EndTime.Subtract(comRes.StartTime) + " seconds");
					result.CommandResults.Add(comRes);
					CommonLib.Tracer.EndActivity("Execute Commands");
				}
			}
			return true;
		}

		/// <summary>
		/// Fired when the batch job completes all executions
		/// </summary>
		public BatchJobCompleteEvent OnBatchJobComplete;
		/// <summary>
		/// Fired when the batch job starts; just before downloading resources
		/// </summary>
		public BatchJobStartEvent OnBatchJobStart;
		/// <summary>
		/// Fired when a resource is completely downloaded from server
		/// </summary>
		public BatchJobDownloadResource OnBatchJobDownloadResource;
		/// <summary>
		/// Fired when a command begins execution
		/// </summary>
		public BatchJobCommandStep OnBatchJobCommandStep;
	}
	public delegate void BatchJobStartEvent(Guid batchId);
	public delegate void BatchJobDownloadResource(Guid batchId, String path);
	public delegate void BatchJobCommandStep(Guid batchId, String command, String arguments);
	public delegate void BatchJobCompleteEvent(ServerNodeLib.ResultInfo info);
}
